/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.nViron.nvTiles;

import instar.dg.Common.ID;
import instar.dg.Common.Point2D;
import instar.dg.Common.Position;
import instar.dg.Debug.Debug;
import instar.dg.Tiles.Tile;
import instar.dg.Tiles.TileFactory;
import instar.dg.Tiles.TiledMapInfo;
import instar.dg.Tiles.TilesCache;
import instar.dg.Tiles.TilesCacheListener;
import instar.dg.Tiles.Projections.NotOnMapException;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.nViron.nvCommon.nvPosition;
import org.nViron.nvGraffiti.nvGraffitiSource;
import org.nViron.nvGraffiti.nvGraffitiSourceListener;
import org.nViron.nvMaps.nvMapSource;
import org.nViron.nvRender.nvAABox;
import org.nViron.nvRender.nvFrustum;
import org.nViron.nvRender.nvRenderer;
import org.nViron.nvRender.nvVec3;
import org.nViron.nvSim.nvSimulator;

/**
 * MapSource implementation for Digital Graffiti Tile Service
 * @author Roland Haring <p>
 * <script type="text/javascript" src="http://www.ohloh.net/p/376434/widgets/project_basic_stats.js"></script>
 */
public abstract class nvTileSource extends nvMapSource implements nvGraffitiSourceListener {

    protected class Cache {

        protected Map cacheables = new HashMap();

        void add(Cacheable cacheable) {
            int hash = cacheable.hashCode();
            Object obj = cacheables.get(hash);
            if (obj != null) {
                Vector vec = (Vector) obj;
                vec.add(cacheable); // I don't check duplicates here!!
            } else {
                Vector vec = new Vector();
                vec.add(cacheable);
                cacheables.put(hash, vec);
            }
        }

        // TODO add some size limit and a cleanup algo as well ....

        Cacheable get(int x, int y, long mapID) {
            int hash = getHashCode(x, y, mapID);
            Object obj = cacheables.get(hash);
            if (obj != null) {
                Vector vec = (Vector) obj;
                Iterator it = vec.iterator();
                while (it.hasNext()) {
                    Cacheable cacheable = (Cacheable) it.next();
                    if (cacheable.getX() == x && cacheable.getY() == y && cacheable.getMapID() == mapID) {
                        return cacheable;
                    }
                }
            }
            return null;
        }

        void remove(int x, int y, long mapID) {
            int hash = getHashCode(x, y, mapID);
            Object obj = cacheables.get(hash);
            if (obj != null) {
                Vector vec = (Vector) obj;
                Iterator it = vec.iterator();
                while (it.hasNext()) {
                    Cacheable cacheable = (Cacheable) it.next();
                    if (cacheable.hashCode() == hash) {
                        it.remove();
                        return;
                    }
                }
            }
        }
    }
    protected class Cacheable {

        protected int x, y;
        protected long mapID;

        public Cacheable(int x, int y, long mapID) {
            this.x = x;
            this.y = y;
            this.mapID = mapID;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if ((obj == null) || (obj.getClass() != this.getClass())) {
                return false;
            }
            // object must be Test at this point
            Cacheable test = (Cacheable) obj;
            return x == test.x && y == test.y && mapID == test.mapID;
        }

        public long getMapID() {
            return mapID;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

        @Override
        public int hashCode() {
            return getHashCode(x, y, mapID);
        }
    }
    public class TilesCacheListenerImpl implements TilesCacheListener {

		public void onError(String msg, Throwable t) {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        public void stateChanged(int oldState, int newState) {
            Debug.out.println("Tileclient state changed from " + oldState + " to " + newState);
        }

        public void tiledMapInfoUpdated(TiledMapInfo[] infos) {
            maps = infos;
            for (int i = 0; i < infos.length; i++) {
                if (infos[i].getMapId() == config.getMapID()) {
                    currentMap = infos[i];
                    forceUpdate();
                }
            }
        }

        public void tileLoaded(Tile t) {
            addTile(t);
        }
    }
    protected class TileTex extends Cacheable {

        protected ByteBuffer buffer;
        protected int width;
        protected int height;

        public TileTex(int x, int y, long mapID) {
            super(x, y, mapID);
        }

        public ByteBuffer getBuffer() {
            return buffer;
        }

        public int getHeight() {
            return height;
        }

        public int getWidth() {
            return width;
        }

        protected void releaseBuffer() {
            setBuffer(null, 0, 0);
        }

        public void setBuffer(ByteBuffer buffer, int width, int height) {
            this.buffer = buffer;
            this.width = width;
            this.height = height;
        }
    }
    protected abstract class VisibleTile extends Cacheable {

        protected nvAABox aaBox;

        public VisibleTile(int x, int y, long mapID) {
            super(x, y, mapID);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if ((obj == null) || (obj.getClass() != this.getClass())) {
                return false;
            }
            // object must be Test at this point
            VisibleTile test = (VisibleTile) obj;
            return x == test.x && y == test.y && mapID == test.mapID;
        }

        public nvAABox getAABox() {
            return aaBox;
        }

        @Override
        public int hashCode() {
            return getHashCode(x, y, mapID);
        }

        protected abstract void render();

        public void setAABox(nvAABox aaBox) {
            this.aaBox = aaBox;
        }
    }
    protected TiledMapInfo currentMap;
    protected TiledMapInfo[] maps;
    protected VisibleTile[][] visibleTiles;
    protected final Cache visibleTileCache = new Cache();

    protected final Cache textureCache = new Cache();
    protected int numX;

    protected int numY;

    private nvRenderer renderer;

    private nvTileSourceConfig config;

    static int DEFAULT_PORT = 19290;


    static int DEFAULT_TIMEOUT = 10000;

    public nvTileSource(nvSimulator simulator, nvGraffitiSource graffitiSource) {
        super(simulator);
        config = (nvTileSourceConfig)simulator.getConfig();
        graffitiSource.addListener(this);
    }

    abstract protected void addTile(Tile tile);

    protected abstract VisibleTile createVisibleTile(int x, int y, long mapId);

    protected nvVec3 geoToOSM(nvPosition pos, float zoom, float subDiv) {
        nvVec3 ret = new nvVec3();
        ret.x = ((float) pos.getLongitude() + 180.0f) / 360.0f * (float) Math.pow(2.0, zoom + subDiv);
        ret.y = pos.getAltitude();
        ret.z = (1.0f - (float) (Math.log(Math.tan(pos.getLatitude() * Math.PI / 180.0) + 1.0f / Math.cos(pos.getLatitude() * Math.PI / 180.0)) / Math.PI)) / 2.0f * (float) Math.pow(2.0, zoom + subDiv);
        return ret;
    }

    protected int getHashCode(int x, int y, long mapID) {
        return x * 10 - y + (int) mapID;
    }

    @Override
    public boolean init() {
        if (!initTilesCache(config.getTileserverIP(), config.isUsePersistentTileCache(), config.getTileCachePath())) {
            return false;
        }
        return true;
    }

    protected boolean initTilesCache(String serverIP, boolean isPersistent, String cachePath) {
        //#mdebug
        Debug.out.println(System.currentTimeMillis() + " ms - [" + Thread.currentThread().getName() + "] init TilesCache()");
        //#enddebug
        if (isPersistent) {
            try {
                nvTilesCacheSE.init(serverIP, DEFAULT_PORT, cachePath);
                TilesCache.addListener(new TilesCacheListenerImpl());
            } catch (Exception e) {
                Debug.out.println("TilesCache could not be initialized!!");
                e.printStackTrace();
                return false;
            }
        } else {
            nvTilesCacheMemorySE.init(serverIP, DEFAULT_PORT);
            TilesCache.addListener(new TilesCacheListenerImpl());
        }
        TilesCache.getInstance().setTimeout(DEFAULT_TIMEOUT);
        TileFactory.factory = new TileFactory();
        return true;
    }

    protected nvPosition OSMToGeo(nvVec3 vec, float zoom, float subDiv) {
        nvPosition ret = new nvPosition();
        ret.setLongitude((vec.x / (float) Math.pow(2.0, zoom + subDiv)) * 360.0 - 180.0);
        double n = Math.PI - (2.0 * Math.PI * (vec.z / (float) Math.pow(2.0, zoom + subDiv)));
        ret.setLatitude(180.0 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))));
        ret.setAltitude(vec.y);
        return ret;
    }

    @Override
    public void render() {
        int culledTiles = 0;
        for (int x = 0; x < numX; x++) {
            for (int y = 0; y < numY; y++) {
                // TODO optimize by testing a bounding sphere first (each bounding box shoudl have a bounding sphere as well); bounding sphere test is cheaper
                if (visibleTiles[x][y].getAABox() != null && renderer.getFrustum().boxInFrustum(visibleTiles[x][y].getAABox()) == nvFrustum.ALIGN.OUTSIDE) {
                    culledTiles++;
                    continue;
                }
                visibleTiles[x][y].render();
            }
        }
    }

    void setRenderer(nvRenderer renderer) {
        this.renderer = renderer;
    }

    public void setUserID(ID userID) {
        try {
            TilesCache.getInstance().setUserID(userID);
            TilesCache.getInstance().requestTileInfoUpdate(new Position(simulator.getUserPosition().getLongitude(), simulator.getUserPosition().getLatitude(), simulator.getUserPosition().getAltitude()), 10.0f);
        } catch (IOException ex) {
            Logger.getLogger(nvTileSource.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    @Override
    protected void updateAreaOfInterest() {
        Point2D ulIdx, lrIdx;
        if (currentMap != null) {
            try {
                ulIdx = currentMap.getProjection().getTileXY(new Position(ul.getLongitude(), ul.getLatitude(), ul.getAltitude()), currentMap);
                lrIdx = currentMap.getProjection().getTileXY(new Position(lr.getLongitude(), lr.getLatitude(), lr.getAltitude()), currentMap);
            } catch (NotOnMapException ex) {
                Logger.getLogger(nvTileSource.class.getName()).log(Level.SEVERE, null, ex);
                return;
            }

            // to avoid rounding issues
            lrIdx.setX(lrIdx.getX() + 1);
            lrIdx.setY(lrIdx.getY() + 1);

            numX = lrIdx.getX() - ulIdx.getX();
            numY = lrIdx.getY() - ulIdx.getY();
            visibleTiles = new VisibleTile[numX][numY];

            // due to a not working Projection implementation we have to do calculations here on our own
            // only implemented for mercator currently ....
            float subDiv = (float) (Math.log(256 / currentMap.getTilesWidthPixel()) / Math.log(2));

            for (int x = ulIdx.getX(); x < lrIdx.getX(); x++) {
                for (int y = ulIdx.getY(); y < lrIdx.getY(); y++) {
                    VisibleTile visibleTile = (VisibleTile) visibleTileCache.get(x, y, currentMap.getMapId());
                    if (visibleTile == null) {
                        visibleTile = createVisibleTile(x, y, currentMap.getMapId());

                        // #if 0
                        // actually this code from graffiti framework should work, but it does not cause the subdivion of tiles is not taking into account!!
                        // Position ul, lr;
                        // try {
                        // ul = currentMap.getProjection().getGeoPosition(x, y, currentMap);
                        // lr = currentMap.getProjection().getGeoPosition(x + 1, y + 1, currentMap);
                        // } catch (NotOnMapException ex) {
                        // Logger.getLogger(TileSource.class.getName()).log(Level.SEVERE, null, ex);
                        // continue;
                        // }
                        // problem here: each AABox is outside of the frustum (subdivision is not taking into account)!!
                        // visibleTile.setAABox(new nvAABox(new nvPosition(ul.getLongitude(), ul.getLatitude(), 0.0f), new nvPosition(lr.getLongitude(), lr.getLatitude(), 1.0f)));
                        // #else
                        // therefore we need a reimplementation here
                        nvPosition ulPos, lrPos;
                        // OSM to VIZ direct conversion would save some calculations ...
                        ulPos = OSMToGeo(new nvVec3(x, 0, y), currentMap.getZoomLevel(), subDiv);
                        lrPos = OSMToGeo(new nvVec3(x + 1, 0, y + 1), currentMap.getZoomLevel(), subDiv);
                        visibleTile.setAABox(new nvAABox(ulPos, lrPos));
                        // #endif
                        visibleTileCache.add(visibleTile);
                    }
                    visibleTiles[x - ulIdx.getX()][y - ulIdx.getY()] = visibleTile;
                }
            }
        }
    }
}
